#include <sockets/tls.hpp>
#include <websocketpp.hpp>

#include <ConnectionBroker/Application.h>
#include <ConnectionBroker/BasicAuthenticator.h>
#include <ConnectionBroker/Channels.h>
#include <ConnectionBroker/PacketUtils.h>
#include <ConnectionBroker/clientconnection/ClientConnection.h>
#include <ConnectionBroker/server/LongPollServer.h>
#include <ConnectionBroker/server/WebSocketServer.h>
#include <ConnectionBroker/server/TcpServer.h>

#include <limits.h>

using namespace ConnectionBroker;

Application::Application()
:  mOnMessageCallback(NULL), 
   mNextRegisteredChannel(1), 
   mVersion("1.0.0"), 
   mAuthenticator(new BasicAuthenticator()),
   mContext(boost::shared_ptr<boost::asio::ssl::context>())
{
   registerChannel("Library_Version");
   registerChannel("Library_Channel_List");
   registerChannel("Library_Channel_Subscription");
   registerChannel("Library_Ping_Pong");
   registerChannel("Library_Authentication");
}

Application::~Application()
{
}

void Application::registerChannel(const std::string& name, applicationCallback pChannelCallback)
{
   if(mRegisteredChannels.count(name))
      mRegisteredChannels.erase(name);

   ChannelPtr pChannel(new Channel(name, mNextRegisteredChannel++, pChannelCallback));
   mRegisteredChannels[name] = pChannel; 
}

void Application::removeChannel(const std::string& name)
{
   mRegisteredChannels.erase(name);
}

void Application::removeOnMessageCallback()
{
   mOnMessageCallback = NULL;
}

void Application::removeServerOnMessageCallback(ServerPtr pServer)
{
   pServer->removeOnMessageCallback();
}

void Application::setAuthenticator(AuthenticatorPtr pAuthenticator)
{
   mAuthenticator = pAuthenticator;
}

void Application::setSslInfo(const std::string& certChainFile, const std::string& privateKeyFile, const std::string& tmpDhFile)
{
   mContext.reset(new boost::asio::ssl::context(boost::asio::ssl::context::sslv23));
   mContext->set_options(boost::asio::ssl::context::default_workarounds
        | boost::asio::ssl::context::no_sslv2
        | boost::asio::ssl::context::single_dh_use);
    mContext->use_certificate_chain_file(certChainFile);
    mContext->use_private_key_file(privateKeyFile, boost::asio::ssl::context::pem);
    mContext->use_tmp_dh_file(tmpDhFile);
}

void Application::setOnClientConnectCallback(applicationCallback pCallback)
{
   mOnClientConnectCallback = pCallback;
}

void Application::setOnClientDisconnectCallback(applicationCallback pCallback)
{
   mOnClientDisconnectCallback = pCallback;
}

void Application::setOnMessageCallback(applicationCallback pCallback)
{
   mOnMessageCallback = pCallback;
}

void Application::setServerOnClientConnectCallback(ServerPtr pServer, applicationCallback pCallback)
{
   pServer->setOnClientConnectCallback(boost::bind(pCallback, shared_from_this(), _1, _2, _3));
}

void Application::setServerOnClientDisconnectCallback(ServerPtr pServer, applicationCallback pCallback)
{
   pServer->setOnClientDisconnectCallback(boost::bind(pCallback, shared_from_this(), _1, _2, _3));
}

void Application::setServerOnMessageCallback(ServerPtr pServer, applicationCallback pCallback)
{
   pServer->setOnMessageCallback(boost::bind(pCallback, shared_from_this(), _1, _2, _3)); 
}

void Application::invokeOnClientConnectCallback(ServerPtr pServer, ClientConnectionPtr pClient, const Packet& pMessage)
{
   //Send the version to the client.
   pClient->write(packVersion());

   //Send the list of channels to the client.
   pClient->write(packChannelList());

   if(!mOnClientConnectCallback)
      return;

   mOnClientConnectCallback(shared_from_this(), pServer, pClient, pMessage);
}

void Application::invokeOnClientDisconnectCallback(ServerPtr pServer, ClientConnectionPtr pClient, const Packet& pMessage)
{
   if(!mOnClientDisconnectCallback)
      return;

   mOnClientDisconnectCallback(shared_from_this(), pServer, pClient, pMessage);
}

void Application::invokeOnMessageCallback(ServerPtr pServer, ClientConnectionPtr pClient, const Packet& pMessage)
{
   //Check to see if this is a channel subscription message.
   if(pMessage.channel() == Channels::LIBRARY_CHANNEL_SUBSCRIPTION)
   {
      handleChannelSubscription(pClient, pMessage);
      return;
   }

   //Check to see if this is a channel ping pong and echo the message if so.
   if(pMessage.channel() == Channels::LIBRARY_PING_PONG)
   {
      pClient->write(pMessage);
      return;
   }

   //Check to see if this is a channel ping pong and echo the message if so.
   if(pMessage.channel() == Channels::LIBRARY_AUTHENTICATION)
   {
      handleAuthentication(pClient, pMessage);
      return;
   }

   //If we're in a library reserved channel, don't execute the callback.
   if(pMessage.channel() <= Channels::PREDEFINED_MAX)
      return;

   //Get the channel and execute the channel specific callback if it exists. If there is a channel specific callback don't call the default callback.
   ChannelPtr pChannel(channelFromNumber(pMessage.channel()));
   if(pChannel)
   {
      applicationCallback pChannelCallback = pChannel->channelCallback();
      if(pChannelCallback)
      {
         pChannelCallback(shared_from_this(), pServer, pClient, pMessage);
         return;
      }
   }

   //Early return if there's no on message callback.
   if(!mOnMessageCallback)
      return;

   mOnMessageCallback(shared_from_this(), pServer, pClient, pMessage);
}

ChannelPtr Application::channelFromNumber(const uint32_t channelNumber)
{
   for(std::map<std::string, ChannelPtr>::iterator it = mRegisteredChannels.begin(); it != mRegisteredChannels.end(); ++it)
   {
      ChannelPtr pChannel(it->second);
      if(pChannel->channelNumber() != channelNumber)
         continue;

      return it->second;
   }

   ChannelPtr nullPtr;
   return nullPtr;
}

ServerPtr Application::tcpServerFactory(const ServerSettings& settings)
{
   TcpServerPtr pServer(
         new TcpServer(
            settings, 
            mContext,
            boost::bind(&Application::invokeOnMessageCallback, shared_from_this(), _1, _2, _3),
            boost::bind(&Application::invokeOnClientConnectCallback, shared_from_this(), _1, _2, _3),
            boost::bind(&Application::invokeOnClientDisconnectCallback, shared_from_this(), _1, _2, _3)
            ));

   mThreads.insert(
      boost::shared_ptr<boost::thread>(
         new boost::thread(boost::bind(&TcpServer::start, pServer))
      )
   );

   mServers.insert(pServer);
   return pServer;

}

ServerPtr Application::webSocketServerFactory(const ServerSettings& settings)
{
   if(mContext)
   {
      boost::shared_ptr<WebSocketServer<websocketpp::server_tls> > pServer(new WebSocketServer<websocketpp::server_tls>(settings, 
               mContext,
               boost::bind(&Application::invokeOnMessageCallback, shared_from_this(), _1, _2, _3),
               boost::bind(&Application::invokeOnClientConnectCallback, shared_from_this(), _1, _2, _3),
               boost::bind(&Application::invokeOnClientDisconnectCallback, shared_from_this(), _1, _2, _3)
              )
      );
      mThreads.insert(
         boost::shared_ptr<boost::thread>(
            new boost::thread(boost::bind(&WebSocketServer<websocketpp::server_tls>::start, pServer))
         )
      );

      mServers.insert(pServer);
      return pServer;
   }
   boost::shared_ptr<WebSocketServer<websocketpp::server> >  pServer(new WebSocketServer<websocketpp::server>(
                        settings,
                        mContext,
                        boost::bind(&Application::invokeOnMessageCallback, shared_from_this(), _1, _2, _3),
                        boost::bind(&Application::invokeOnClientConnectCallback, shared_from_this(), _1, _2, _3),
                        boost::bind(&Application::invokeOnClientDisconnectCallback, shared_from_this(), _1, _2, _3)
                       ));
   mThreads.insert(
      boost::shared_ptr<boost::thread>(
         new boost::thread(boost::bind(&WebSocketServer<websocketpp::server>::start, pServer))
      )
   );

   mServers.insert(pServer);
   return pServer;
}

ServerPtr Application::longPollServerFactory(const ServerSettings& settings)
{
   LongPollServerPtr pServer(new LongPollServer(settings, 
                                                  boost::bind(&Application::invokeOnMessageCallback, shared_from_this(), _1, _2, _3),
                                                  boost::bind(&Application::invokeOnClientConnectCallback, shared_from_this(), _1, _2, _3),
                                                  boost::bind(&Application::invokeOnClientDisconnectCallback, shared_from_this(), _1, _2, _3)
                                                 ));

   mThreads.insert(
      boost::shared_ptr<boost::thread>(
         new boost::thread(boost::bind(&LongPollServer::start, pServer))
      )
   );

   mServers.insert(pServer);
   return pServer;
}

void Application::handleAuthentication(ClientConnectionPtr pSender, const Packet& pPacket)
{
   if(pSender->authenticated())
      return;

   if(!mAuthenticator)
      return;

   bool authenticated = mAuthenticator->authenticate(pPacket); 
   if(!authenticated)
      return;

   //Authenticate this client.
   pSender->authenticate();

   //Send the client a message to say they were authenticated.
   pSender->write(packAuthenticated());
}

void Application::handleChannelSubscription(ClientConnectionPtr pSender, const Packet& pPacket)
{
   //Don't allow the client do do any subscription until they are authenticated.
   if(!pSender->authenticated())
   {
      std::cout << pSender << " is not authenticated" << std::endl;
      return;
   }

   Json::Value pMsgJson(pPacket.message());
   Json::Value channelList(pMsgJson.get("channels", Json::nullValue));
   if(channelList.isNull())
      return;

   std::vector<uint32_t> channels;
   for(int i = 0; i < channelList.size(); ++i)
   {
      uint32_t channel = channelList[i].asUInt();
      channels.push_back(channel);
   }

   //If an unsubscribe property is there and is true, unsubscribe instead of subscribe.
   Json::Value unsubscribeJson(pMsgJson.get("unsubscribe", Json::nullValue));
   if(unsubscribeJson.isNull() || !unsubscribeJson.asBool())
      pSender->subscribeTo(channels);
   else
      pSender->unsubscribeFrom(channels);
}

Packet Application::packChannelList()
{
   Json::Value pMsgJson(Json::objectValue);
   Json::Value channelList(Json::arrayValue);
   for(std::map<std::string, ChannelPtr>::iterator it = mRegisteredChannels.begin(); it != mRegisteredChannels.end(); ++it)
   {
      Json::Value channel(Json::objectValue);
      std::string name(it->first);
      uint32_t value = (it->second->channelNumber());
      channel[name] = value;
      channelList.append(channel);
   }
   pMsgJson["channels"] = channelList;
   return Packet(Channels::LIBRARY_CHANNEL_LIST, pMsgJson);
}

Packet Application::packAuthenticated()
{
   Json::Value pMsgJson(Json::objectValue);
   pMsgJson["authenticated"] = std::string("true");
   return Packet(Channels::LIBRARY_AUTHENTICATION, pMsgJson);
}

Packet Application::packVersion()
{
   Json::Value pMsgJson(Json::objectValue);
   pMsgJson["version"] = mVersion;
   return Packet(Channels::LIBRARY_VERSION, pMsgJson);
}

void Application::writeToAll(const Packet& pMessage)
{
   for(std::set<ServerPtr>::iterator it = mServers.begin(); it != mServers.end(); ++it)
      (*it)->writeToAll(pMessage);
}

uint32_t Application::channelNumber(const std::string& channelName)
{
   if(!mRegisteredChannels.count(channelName))
      return UINT_MAX;

   return mRegisteredChannels[channelName]->channelNumber();
}

uint32_t Application::numberOfClientsSubscribedTo(const std::string& channelName)
{
   if(!mRegisteredChannels.count(channelName))
      return 0;

   uint32_t channelNumber = mRegisteredChannels[channelName]->channelNumber();
   uint32_t numClients = 0;
   for(std::set<ServerPtr>::iterator it = mServers.begin(); it != mServers.end(); ++it)
      numClients += (*it)->numberOfClientsSubscribedTo(channelNumber);

   return numClients;
}
